package spl.army;

import java.security.acl.LastOwnerException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Formatter;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;

import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;

import spl.utils.GlobalLogger;

/**
 * Sergeant is a thread pool manager.
 * It supports queries on it's threads, assigning new threads etc.
 * This is mostly a synchronized object (except for methods that regards
 * unchanged information).
 * Key concept of the sergeant is that synched methods do not wait() - i.e - no
 * blocking is enforced from within it, so the program will always be able to
 * submit new threads or gracefully stop them. In other words - Sergeant instance must
 * stay a responsive through all the life cycle of the simulation.
 *  
 * @author ruby, avikam
 *
 */
public class Sergeant{
	
	public static final int SLEEP_INTERVAL = 1000;
	
    private class TaskCallback implements FutureCallback<Mission> {
    	@Override
        public void onSuccess(Mission mission) {
    		GlobalLogger.getLogger().finest("Mission " + mission.toString() + " is finished");
    		
    		Sergeant sergeant = mission.getAssignedSergeant();
    		
            sergeant.taskFinised();
            Board.updateMission(mission);
        }

        @Override
        public void onFailure(Throwable t) {
        	// Never fails...
        }
    }	
	
	 private class Task implements Callable<Mission> {
	        private Mission mission;
			private int workHours;
			private Sergeant sergeant;
	        
	        public Task(Sergeant sergeantResponsible, Mission missionParam, int workHoursParam) {
	        	this.mission = missionParam;
	        	this.workHours = workHoursParam;
	        	this.sergeant = sergeantResponsible;
	        }
	        
	        @Override
	        public Mission call() throws Exception {
	        	GlobalLogger.getLogger().finest(this.mission.toString() + ": " + this.sergeant.getName() + " is aquiring items");
	        	
	    		// Acquire items
	    		ArrayList<Item> acquiredItems = new ArrayList<Item>();
	    		
	    		Iterator<ItemRequired> it = this.mission.getItemsRequired().iterator();
	    		while(it.hasNext()){
	    			ItemRequired current = it.next();
	    			// 'takeItem' is blocking
	    			try{
	    				Item t = Warehouse.takeItem(current, this.sergeant);
	    				acquiredItems.add(t);	    				
	    			}catch(TimeoutException e)
	    			{
	    				GlobalLogger.getLogger().finest(this.mission.toString() + ": " + this.sergeant.getName() + " timeout on aqquiring items, release all items");
	    				this.mission.disableActive();
	    				Warehouse.returnItems(acquiredItems, this.sergeant);
	    				return this.mission;
	    			}
	    			

	    		}
	        	
	    		// Start simulation
	    		GlobalLogger.getLogger().finest(this.mission.toString() + ": " + this.sergeant.getName() + " is sarting mission");
	        	int actualWork = 0;
	        	while (actualWork < this.workHours & this.mission.isActive() ) {
	        		Thread.sleep(Sergeant.SLEEP_INTERVAL);
	        		actualWork += 1;
	        	}
	        	
	        	// Simulation stopped. return items
	        	Warehouse.returnItems(acquiredItems, this.sergeant);
	        	
	            this.mission.updateTimeLeft(actualWork);
	            this.mission.disableActive();
	            
	            return this.mission;
	        }
	 }

	// Details on sergeant
	private String name;
	private int missionsCountLeft;
	private ArrayList<String> skills;
	private int maxWorkHours;
	private int numberOfThreads;
	
	// Thread pool manager:
	private ListeningExecutorService executorService;
	// Threads:
	private ArrayList<ListenableFuture<Mission>> allTasks;
	
	// Counter of active threads:
	private int currentlyRunningThread;
	
	// Missions assigned to this sergeant
	private Set<Mission> uniqueMissionList;
	
	// Supports graceful unload of the sergeant 
	private boolean isOperational;
	
	/*
	 * priorityOrder can be:
	 * 	1) shortestMission or longestMission
	 *  2) minItems or maxItems
	 */
	private String priorityOrder;
	
	private void initSergeant(int maxMissions,
					ArrayList<String> sSkills, int nMaxWorkHours,
					String sPriorityOrder,int nNumberOfThreads){
		this.missionsCountLeft = maxMissions;
		this.skills = sSkills;
		this.maxWorkHours = nMaxWorkHours;
		this.priorityOrder = sPriorityOrder;
		this.numberOfThreads = nNumberOfThreads;	
		this.uniqueMissionList = new HashSet<Mission>();
		this.allTasks = new ArrayList<ListenableFuture<Mission>>();
		this.executorService = MoreExecutors.listeningDecorator(
								Executors.newFixedThreadPool(this.numberOfThreads));
		
		
		// Initialize currently running threads
		this.currentlyRunningThread = 0;
		// Let the sergeant work
		this.isOperational = true;
		
		GlobalLogger.getLogger().finest("Sergeant " + this.name + " is created");
	}
	

	/**
	 * @param sergeantName is the name of the sergeant
	 * @param nMaxMissions is a number of maximum missions
	 * @param arrSkills is the skill of the sergeant
	 * @param nMaxWorkHours as the maximum of hours the sergeant can work
	 * @param sPriorityOrder is the priority order (shortestMission, shortestMission, minItems, maxItems)
	 * @param nNumberOfThreads is number of maximum missions at a time
	 */
	public Sergeant(String sergeantName, int nMaxMissions, ArrayList<String> arrSkills, int nMaxWorkHours,
			String sPriorityOrder,int nNumberOfThreads) {
		this.name = sergeantName;
		this.initSergeant( nMaxMissions,
							arrSkills,
							nMaxWorkHours,
							sPriorityOrder,
							nNumberOfThreads);
	}
	
	/**
	 * @param sergeantName is the name of the sergeant
	 * @param maxMissionsStr is a number of maximum missions
	 * @param skillsStr is the skill of the sergeant. i.e (skill1, skill2...) or (skill1 skill2...)
	 * @param maxWorkHoursStr as the maximum of hours the sergeant can work
	 * @param sPriorityOrder is the priority order (shortestMission, shortestMission, minItems, maxItems)
	 * @param numberOfThreadsStr is number of maximum missions at a time
	 */
	public Sergeant(String sergeantName, String maxMissionsStr, String skillsStr, String maxWorkHoursStr,
			String sPriorityOrder, String numberOfThreadsStr) {
		
		this.name = sergeantName;
		int maxMissions = Integer.decode(maxMissionsStr.trim());
		ArrayList<String> skillsList = new ArrayList<String>(
							Arrays.asList(skillsStr.split("[,\\s]+"))
							);
		for(int i=0;i<skillsList.size();i++) {
			skillsList.set(i, skillsList.get(i).trim());
		}

		this.initSergeant(maxMissions,
							skillsList,
							Integer.decode(maxWorkHoursStr.trim()),
							sPriorityOrder,
							Integer.decode(numberOfThreadsStr.trim()));
	}
	

	/**
	 * Send the sergeant another mission to work on!.
	 * Note - This function is called from Chief's thread, so it must
	 * return immediately, and not wait.
	 * @param mission to assign
	 */
	public synchronized void giveMission(Mission mission)
	{
		GlobalLogger.getLogger().finest("'" + mission.getMissionName() + "' is assigned to " + this.name);
		
		try {
			// optimistic assignment
			if (mission.assignSergeant(this)) {
				// if no one sergeant was assigned to this mission,
				// update my mission count.
				this.missionsCountLeft -= 1;
			}
		} catch (LastOwnerException e) {
			GlobalLogger.getLogger().warning("Can't assign mission " + mission.toString() + " to " + toString());
			return;
		}

		// Register the mission
		this.uniqueMissionList.add(mission);
		
		// Will work on the mission as much as possible
		int workHours = Math.min( mission.getTimeLeft(), this.maxWorkHours );
		
		// Create mission's thread
		this.currentlyRunningThread += 1;
		ListenableFuture<Mission> futureTask = this.executorService.submit(
					new Task(this, mission, workHours) 
				);
		this.allTasks.add(futureTask);
		TaskCallback callback = new TaskCallback();
		Futures.addCallback(futureTask, callback);
	}

	/**
	 * @return read only view of missions assigned to the sergeant
	 */
	public synchronized Set<Mission> getAllAssignedMissions(){
		return Collections.unmodifiableSet(this.uniqueMissionList);
	}
	
	/**
	 * @return the number of missions active
	 */
	public synchronized int getActiveMissionsCount() {
		return this.currentlyRunningThread;
	}
	
	/**
	 * Update currently running threads parameter.
	 */
	public synchronized void taskFinised() {
		// Decrement the amount of missions 
		this.currentlyRunningThread -=1;
	}
	
	/**
	 * @param m mission that sergeant will check if he can accept.
	 * @return true if can except the mission,
	 * has free threads, and didn't exceed missions count, and wasn't
	 * set off by user
	 */
	public boolean ableToWork(Mission m){
		// Return false if sergeant is not operational or mission is not a mission
		if (!this.isOperational | m == null)
			return false;
		
		int activeMissions = this.getActiveMissionsCount();
		// Currently active missions must not exceed the amount of missions
		// allowed at the same time
		if (this.getNumberOfThreads() <= activeMissions)
			return false;
		
		// Sergeant can work on more NEW missions
		if(this.getMissionsLeft() <= 0)
				if (false == m.isAssigned())
						return false;
		
		return true;
	}
	
	/**
	 * makes ableToWork() to return false, write after it shut's down the
	 * thread manger executorService.
	 * At the end, the function blocks all other threads as it
	 * iterate over the threads and cancels them. @see stopAllMissions.
	 * 
	 * @return false if at least one of the tasks wasn't stopped. 
	 */
	public boolean stop() {
		// The very first thing to do, is to stop allowing new threads:
		this.executorService.shutdown();
		
		// And then - to disable anyone with the ability to assign new threads:
		this.isOperational = false;
		
		// Stop all missions in synchronized manner
		return stopAllMissions();
	}
	
	/**
	 * Iterate over all threads and stop them.
	 * Then, stops the executor service.
	 * 
	 * @return false if at least one of the tasks wasn't stopped 
	 */
	public synchronized boolean stopAllMissions() {
		GlobalLogger.getLogger().info("Stopping all missions of " + this.name);
		// Iterate over the missions assigned to this sergeant and disable them
		Iterator<Mission> myMissions = this.uniqueMissionList.iterator();
		while(myMissions.hasNext())
			myMissions.next().disableActive();
		
		boolean ret = true;
		// Iterate over all threads and stop them
		Iterator<ListenableFuture<Mission>> it = this.allTasks.iterator();
		while(it.hasNext()){
			ListenableFuture<Mission> currentListenableFuture = it.next();

			// Stop the task if needed
			if (!currentListenableFuture.isDone())
				ret = ret & currentListenableFuture.cancel(true);
		}
		
		return ret;
	}
	
	/**
	 * @return number of threads
	 */
	public int getNumberOfThreads(){
		return this.numberOfThreads;
	}	
	
	/**
	 * @return the missionsCountLeft
	 */
	public synchronized int getMissionsLeft() {
		return this.missionsCountLeft;
	}
	/**
	 * @return the skills
	 */
	public List<String> getSkills() {
		return Collections.unmodifiableList(this.skills);
	}
	/**
	 * @return the maxWorkHours
	 */
	public int getMaxWorkHours() {
		return this.maxWorkHours;
	}
	/**
	 * @return the priorityOrder
	 */
	public String getPriorityOrder() {
		return this.priorityOrder;
	}

	/**
	 * @return the name
	 */
	public String getName() {
		return this.name;
	}
	
	/**
	 * @return string repr of the status
	 */
	public String getSergeantStatus() {
		StringBuilder sb = new StringBuilder();
		Formatter formatter = new Formatter(sb);
		
		return formatter.format("%s - Num Threads: %d, Active Mission Count: %d, Missions Left: %d",
				this.getName(), this.getNumberOfThreads(),
				this.getActiveMissionsCount(), this.getMissionsLeft()).toString();
	}
	
}
